/*
 *	OpenSource code or fragments by avocado systems , Marcus Fuhrmeister
 *
 *	Redistribution and use in source and binary forms, with or without
 *	modification, are permitted provided that the following conditions
 *	are met:
 *  
 *		* Redistribution of source code must retain the above OpenSource comment,
 *		this list of conditions and the following disclaimer.
 * 
 *		* Redistribution in binary form must reproduce the above OpenSource comment,
 *		this list of conditions and the following disclaimer in the
 *		documentation and/or other materials provided with the distribution.
 * 
 *	Neither the name of 'avocaod systems, Marcus Fuhrmeister' or the
 *	names of contributors may be used to endorse or promote products derived
 *	from this software without specific prior written permission.
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *	Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 
 */
package de.fuhrmeister.util;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.ArrayList;

/**
 * File handling class. Provides helper methods to read, write to, serialize,
 * deserialize, erease, check for existence and to extract the file path from
 * Windows link files.
 * 
 * @date 24.08.2010
 * @author Marcus Fuhrmeister
 */
public class FileManager {

	public final int ERROR_FILEDOESNOTEXIST = 1;

	public final int ERROR_FILEALREADYEXISTS = 2;

	public final int ERROR_CANTCOPY = 3;

	public final int ERROR_CANTCLOSEINPUTSTREAM = 4;

	public final int ERROR_CANTCLOSEOUTPUTSTREAM = 5;

	public final int ERROR_STREAMFAILURE = 6;

	public final int NO_ERRORS = 77;

	private static FileManager fileMng;

	private StringBuffer bufferedFile;

	public final String userDir = System.getProperty("user.dir") + System.getProperty("file.separator");

	/**
	 * Singleton pattern.
	 * 
	 * @return this
	 */
	public static synchronized FileManager getInstance() {
		if (fileMng == null) {
			FileManager.fileMng = new FileManager();
			return FileManager.fileMng;
		}
		return FileManager.fileMng;
	}

	/**
	 * Returns a new <code>FileInputStream</code> for the given file name.
	 * 
	 * @param file
	 *            the file name
	 * @return the input stream
	 * @throws FileNotFoundException
	 *             if the file does not exists OR is empty
	 */
	public InputStream openUTF8File(final String file) throws FileNotFoundException {
		final File f = new File(file);
		InputStream fis = null;
		try {
			if (!f.exists() || f.length() == 0) {
				throw new FileNotFoundException("FileManager::openUTF8File() - configuration file or path does not exist");
			} else {
				fis = new FileInputStream(f);
			}
		} catch (final IOException e) {
			throw new FileNotFoundException("FileManager::openUTF8File() - system cannot find the given path - " + file);
		}
		return fis;
	}

	/**
	 * Calls to open a file for the given file name and returns a
	 * <code>StringBuffer</code>.
	 * 
	 * @param file
	 *            the file name
	 * @return the file as <code>StringBuffer</code>
	 * @throws IOException
	 */
	public StringBuffer readUTF8File(final String file) throws FileNotFoundException, IOException {
		bufferedFile = new StringBuffer("");
		final InputStream inputStream = openUTF8File(file);
		final BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
		String currentLine;
		while ((currentLine = br.readLine()) != null) {
			bufferedFile.append(currentLine);
		}
		return bufferedFile;
	}

	/**
	 * Returns a resource for the given file name via the class loader.
	 * 
	 * @param file
	 *            the file name
	 * @return the resource as <code>InputStream</code>
	 */
	public InputStream getResource(final String file) {
		return this.getClass().getClassLoader().getResourceAsStream(file);
	}

	/**
	 * Returns an <code>OutputStream</code> for the given file name via the
	 * class loader.
	 * 
	 * @param file
	 *            the file name
	 * @return the resource as <code>InputStream</code>
	 * @throws IOException
	 * @throws FileNotFoundException
	 */
	public OutputStream getOutputStream(final String file) throws FileNotFoundException, IOException {
		final File f = new File(file);
		if (!f.createNewFile()) {
			return new FileOutputStream(f);
		}
		return null;
	}

	/**
	 * Writes an encoded <code>StringBuffer</code> to a file with the given
	 * filew name.
	 * 
	 * @param encoded
	 *            the encoded <code>StringBuffer</code>
	 * @param file
	 *            the file name
	 * @throws FileNotFoundException
	 * @throws IOException
	 */
	public void newUTF8File(final StringBuffer encoded, final String file) throws IOException {
		final File f = new File(file);
		OutputStream fos = null;
		try {
			fos = new FileOutputStream(f);
			final BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(fos));
			bw.write(encoded.toString());
			bw.flush();
			bw.close();
		} catch (final IOException e) {
			throw new IOException("FileManager::newUTF8File() - unexpected error while streaming file: " + file);
		}
	}

	/**
	 * Creates a folder with the given name and path.
	 * 
	 * @param applicationPath
	 *            the path to the application
	 * @param configFolder
	 *            the configuration folder name
	 * @return true if the folder was created successfully
	 */
	public boolean mkdir(final String applicationPath, final String configFolder) {
		final File folder = new File(applicationPath + System.getProperty("file.separator") + configFolder);
		final boolean returnValue = folder.mkdir();
		return returnValue;
	}

	/**
	 * Writes an object as an <code>ObjectOutputStream</code> to a file with the
	 * given file name.
	 * 
	 * @param obj
	 * @param file
	 */
	public void serialize(final Object obj, final String file) {
		OutputStream fos = null;
		try {
			fos = new FileOutputStream(file);
			final ObjectOutputStream o = new ObjectOutputStream(fos);
			o.writeObject(obj);
		} catch (final IOException e) {
			System.err.println(e);
		} finally {
			try {
				fos.close();
			} catch (final Exception e) {
			}
		}
	}

	/**
	 * Returns an object, read from the <code>ObjectInputStream</code> with the
	 * given file name
	 * 
	 * @param file
	 *            the file name
	 * @return the object deserialized from the the file or null
	 */
	public Object deserialize(final String file) {
		InputStream fis = null;
		try {
			fis = new FileInputStream(file);
			final ObjectInputStream o = new ObjectInputStream(fis);
			return o.readObject();
		} catch (final IOException e) {
			System.err.println(e);
		} catch (final ClassNotFoundException e) {
			e.printStackTrace();
		} finally {
			try {
				fis.close();
			} catch (final Exception e) {
			}
		}
		return null;
	}

	/**
	 * Calls to copy a file from the given source to the given destination
	 * directory.
	 * 
	 * @param sourceDir
	 *            the source directory
	 * @param file
	 *            the file to copy
	 * @param isSourceDirRelativeToUserDir
	 *            if <code>true</code>, source directory is relative to the user
	 *            directory
	 * @param destDir
	 *            the destination directory
	 * @param isDestDirRelativeToUserDir
	 *            if <code>true</code>, destination directory is relative to the
	 *            user directory
	 * @throws IOException
	 */
	public void copy(String sourceDir, final String file, final boolean isSourceDirRelativeToUserDir, String destDir, final boolean isDestDirRelativeToUserDir) throws IOException {
		if (isSourceDirRelativeToUserDir) {
			sourceDir = userDir + System.getProperty("file.separator") + sourceDir + System.getProperty("file.separator");
		} else {
			sourceDir += System.getProperty("file.separator");
		}
		if (isDestDirRelativeToUserDir) {
			destDir = userDir + System.getProperty("file.separator") + destDir + System.getProperty("file.separator");
		} else {
			destDir += System.getProperty("file.separator");
		}
		sourceDir += file;
		destDir += file;
		copyFile(sourceDir, destDir);
	}

	/**
	 * Copies an <code>InputStream</code> to an <code>OutputStream</code>
	 * 
	 * @param in
	 *            the <code>InputStream</code>
	 * @param out
	 *            the <code>OutputStream</code>
	 * @throws IOException
	 */
	public void copy(final InputStream in, final OutputStream out) throws IOException {
		final byte[] buffer = new byte[0xFFFF];

		for (int len; (len = in.read(buffer)) != -1;) {
			out.write(buffer, 0, len);
		}
	}

	/**
	 * Copies a file from a source location to a destination location.
	 * 
	 * @param src
	 *            the source location
	 * @param dest
	 *            the destination location
	 * @throws IOException
	 */
	public void copyFile(final String src, final String dest) throws IOException {
		FileInputStream fis = null;
		FileOutputStream fos = null;
		try {
			fis = new FileInputStream(src);
			fos = new FileOutputStream(dest);
			copy(fis, fos);
		} catch (final FileNotFoundException e) {
			throw e;
		} finally {
			if (fis != null) {
				try {
					fis.close();
				} catch (final IOException e) {
				}
			}
			if (fos != null) {
				try {
					fos.close();
				} catch (final IOException e) {
				}
			}
		}
	}

	/**
	 * Deletes a file.
	 * 
	 * @param file the absolute file name
	 */
	public void erase(final String file) {
		final File f = new File(file);
		f.delete();
	}

	/**
	 * Deletes all empty files in the given directory with the given file extension.
	 * 
	 * @param dir the directory
	 * @param file_ext the file extension
	 */
	public void deleteEmptyFiles(final String dir, final String file_ext) {
		final File userdir = new File(dir);
		for (final String entry : userdir.list(new FileManager.Filter(file_ext))) {
			final File file = new File(dir + System.getProperty("separator") + entry);
			if (file.length() == 0) {
				file.delete();
			}
		}
	}

	/**
	 * Checks wether the file for the given file name exists.
	 * 
	 * @param file the file name
	 * @return <code>true</code>, if the file exists
	 */
	public boolean exists(final String file) {
		return new File(file).exists();
	}

	/**
	 * Creates a folder if it not exists.
	 * 
	 * @param folder the folder to create
	 */
	public void initFolder(final String folder) {
		final File f = new File(folder);
		if (!f.exists()) {
			f.mkdir();
		}
	}

	/**
	 * Returns a list of file names in the given directory name and file extension.
	 * 
	 * @param dir the directory
	 * @param file_ext the file extension
	 * @return an <code>ArrayList<String></code> containing the file names
	 */
	public ArrayList<String> getFileNames(final String dir, final String file_ext) {
		final File userdir = new File(dir);
		final ArrayList<String> afiles = new ArrayList<String>();
		for (final String entry : userdir.list(new FileManager.Filter(file_ext))) {
			afiles.add(entry.replaceAll(file_ext, ""));
		}
		return afiles;
	}

	/**
	 * Swing Hacks Tips and Tools for Killer GUIs By Joshua Marinacci, Chris
	 * Adamson
	 * 
	 * @see http://www.oreilly.com/catalog/swinghks/
	 */
	public String parseLink(File f) throws Exception {
		// read the entire file into a byte buffer
		FileInputStream fin = new FileInputStream(f);
		ByteArrayOutputStream bout = new ByteArrayOutputStream();
		byte[] buff = new byte[256];
		while (true) {
			int n = fin.read(buff);
			if (n == -1) {
				break;
			}
			bout.write(buff, 0, n);
		}
		fin.close();
		byte[] link = bout.toByteArray();

		// get the flags byte
		byte flags = link[0x14];

		/*
		 * get the file attributes byte final int file_atts_offset = 0x18; byte
		 * fileatts = link[file_atts_offset]; byte is_dir_mask = (byte)0x10;
		 * if((fileatts & is_dir_mask) > 0) { is_dir = true; } else { is_dir =
		 * false; }
		 */

		// if the shell settings are present, skip them
		final int shell_offset = 0x4c;
		int shell_len = 0;
		if ((flags & 0x1) > 0) {
			// the plus 2 accounts for the length marker itself
			shell_len = bytes2short(link, shell_offset) + 2;
		}

		// get to the file settings
		int file_start = 0x4c + shell_len;

		// get the local volume and local system values
		int local_sys_off = link[file_start + 0x10] + file_start;
		return getNullDelimitedString(link, local_sys_off);
	}

	/**
	 * Swing Hacks Tips and Tools for Killer GUIs By Joshua Marinacci, Chris
	 * Adamson
	 * 
	 * @see http://www.oreilly.com/catalog/swinghks/
	 */
	private String getNullDelimitedString(byte[] bytes, int off) {
		int len = 0;
		// count bytes until the null character (0)
		while (true) {
			if (bytes[off + len] == 0) {
				break;
			}
			len++;
		}
		return new String(bytes, off, len);
	}

	/**
	 * Swing Hacks Tips and Tools for Killer GUIs By Joshua Marinacci, Chris
	 * Adamson
	 * 
	 * @see http://www.oreilly.com/catalog/swinghks/
	 */
	private int bytes2short(byte[] bytes, int off) {
		// due to convention a java byte is a signed value from -128 to 127
		// this converts to unsigned value from 0 to 255 to get the correct file
		int low = (bytes[off] < 0 ? bytes[off] + 256 : bytes[off]);
		int high = (bytes[off + 1] < 0 ? bytes[off + 1] + 256 : bytes[off + 1]) << 8;
		return 0 | low | high;
		// return ((bytes[off + 1] & 0xff) << 8) | (bytes[off] & 0xff);
	}

	/**
	 * Swing Hacks Tips and Tools for Killer GUIs By Joshua Marinacci, Chris
	 * Adamson
	 * 
	 * @see http://www.oreilly.com/catalog/swinghks/
	 */
	public boolean isLink(File file) throws IOException {
		File canonicalDir = file.getParentFile().getCanonicalFile();
		File fileInCanonicalDir = new File(canonicalDir, file.getName());
		return fileInCanonicalDir.getCanonicalFile().equals(fileInCanonicalDir.getAbsoluteFile());
	}

	/**
	 * Implements the <code>FilenameFilter</code>.
	 * 
	 * @date	16.02.2012
	 * @author	Marcus Fuhrmeister
	 */
	private class Filter implements FilenameFilter {

		private final String filter;

		/**
		 * Initializes the file filter with the default file extension.
		 */
		@SuppressWarnings("unused")
		public Filter() {
			this(".txt");
		}

		/**
		 * Constructor to set the given file filter.
		 * 
		 * @param filter the file filter
		 */
		public Filter(final String filter) {
			this.filter = filter;
		}

		public boolean accept(final File dir, final String file) {
			return file.toLowerCase().endsWith(filter);

		}
	}
}
